﻿namespace HuwmanCode.ServiceProcess

open System
open System.ComponentModel
open System.Configuration.Install
open System.Diagnostics
open System.IO
open System.Reflection
open System.ServiceProcess
open System.Text.RegularExpressions
open System.Threading
open HuwmanCode
open HuwmanCode.Diagnostics
open HuwmanCode.UserAccountControl
open HuwmanCode.Diagnostics.Logging
open HuwmanCode.ServiceProcess.DebugSupport

type internal ServiceAction =
| Start
| Stop

type internal Base() =
    static let services = lazy trace "Find services" {
        return Assembly.GetEntryAssembly().GetExportedTypes()
            |> Array.filter(fun t -> t.IsSubclassOf(typeof<WindowsServiceBase>))
            |> Array.map(fun t -> Activator.CreateInstance(t) :?> WindowsServiceBase) }

    static let getDependencies (service:WindowsServiceBase) =
        DependsOnAttribute.GetDependencies service
        |> Array.map(fun d ->
            match d with
            | ServiceName sn -> sn
            | ServiceType st -> 
                try
                    services.Value |> Array.tryFind(fun s -> s.GetType() = st) |> Option.map(fun s -> s.ServiceName) |> Option.get
                with :? ArgumentException -> failwith "The service dependency does not exist!")

    static let serviceInstallers = //Creates a ServiceInstaller for each WindowsServiceBase in Services
        let getDescription (service:WindowsServiceBase) =
            service.GetType().GetCustomAttributes(typeof<DescriptionAttribute>, false)
            |> Array.map(fun o -> o :?> DescriptionAttribute)
            |> Array.map(fun a -> a.Description)
            |> Array.nthOrNone 0
            |> Option.defaultTo service.ServiceName

        lazy trace "Get service installers" { 
            return services.Value
            |> Array.map(fun s -> new ServiceInstaller(ServiceName=s.ServiceName, Description=getDescription s, StartType=ServiceStartMode.Automatic, ServicesDependedOn=getDependencies s))
            |> Array.map(fun si -> si :> Installer)
            |> Array.toList }

    static let getServiceProcessInstalledStatus () =
        trace "Get service process installed status" {
            return services.Value 
            |> Array.exists(fun service ->
                                try
                                    use sc = new ServiceController(service.ServiceName)
                                    let st = sc.ServiceType
                                    st = ServiceType.Win32OwnProcess || sc.ServiceType = ServiceType.Win32ShareProcess || sc.ServiceType = ServiceType.InteractiveProcess
                                with _ -> false) }

    static let performServiceAction action (sc:ServiceController) =
        try
            match action with
            | Stop when sc.Status <> ServiceControllerStatus.Stopped -> 
                sc.Stop()
                sc.WaitForStatus(ServiceControllerStatus.Stopped)
                Some(ServiceControllerStatus.Stopped)
            | Start when sc.Status <> ServiceControllerStatus.Running -> 
                sc.Start()
                sc.WaitForStatus(ServiceControllerStatus.Running)
                Some(ServiceControllerStatus.Running)
            | _ -> None
        with _ -> None

    static let asyncStopAllServices () =
        async { return services.Value 
                       |> Array.fold (fun stopped service ->
                                          use sc = new ServiceController(service.ServiceName)
                                          match performServiceAction Stop sc with
                                          | Some ServiceControllerStatus.Stopped -> service.ServiceName :: stopped
                                          | _ -> stopped) [] }
    
    static let asyncStartAllServices () =
        async { return services.Value 
                       |> Array.fold (fun started service ->
                                          use sc = new ServiceController(service.ServiceName)
                                          match performServiceAction Start sc with
                                          | Some ServiceControllerStatus.Running -> service.ServiceName :: started
                                          | _ -> started) [] }

    static let run () = 
        let (|Name|_|) (proc:Process option) = proc |> Option.map(fun p -> p.ProcessName,p)     
        
        let baseRun () = Base.Services |> Seq.cast<ServiceBase> |> Seq.toArray |> ServiceBase.Run
        let debugRun (debuggerProcess:Process) =
            let qtn = "HuwmanCode.ServiceProcess.DebugSupport.DebuggerAttacher,HuwmanCode.ServiceProcess.DebugSupport"
            try
                let debuggerAttacherType = Type.GetType(qtn)
                let debuggerAttacher = Activator.CreateInstance(debuggerAttacherType) :?> IAttachDebugger
                debuggerAttacher.Attach debuggerProcess |> ignore
            with e -> 
                Debug.WriteLine(sprintf "Unable to attach debugger to service process, unable to load %s" qtn)
                baseRun ()

        let notifyAttacher (serviceProcess:Process) =
            let qtn = "HuwmanCode.ServiceProcess.DebugSupport.AttacherCooperator,HuwmanCode.ServiceProcess.DebugSupport"
            try
                let attacherCooperatorType = Type.GetType(qtn)
                let attacherCooperator = Activator.CreateInstance(attacherCooperatorType) :?> INotifyAttacher
                attacherCooperator.NotifyAttacher serviceProcess
            with e -> 
                Debug.WriteLine(sprintf "Unable to communicate with debugger attacher, unable to load %s" qtn)

        match Process.Current.Parent with
        | Name ("devenv",devenv) when Debugger.IsAttached -> debugRun devenv
        | Name ("services",_) -> 
            notifyAttacher Process.Current
            baseRun ()
        | _ -> baseRun ()

    static member Services = services.Value
    static member GetServiceInstallers () = serviceInstallers.Value
    static member AsyncStopAllServices () = asyncStopAllServices ()
    static member AsyncStartAllServices () = asyncStartAllServices ()
    static member GetServiceProcessInstalledStatus () = getServiceProcessInstalledStatus ()
    static member Run() = run ()        

and [<DesignerCategory("Default")>]
    public WindowsServiceBase(serviceName:string) as this =
    inherit ServiceBase()
    do 
        match serviceName with
        | Text text -> base.ServiceName <- text
        | _ -> base.ServiceName <- this.GetType().FullName

    new () = new WindowsServiceBase(null)

    member internal this.StartAfterInstall
        with get() = StartAfterInstallAttribute.IsDecorated(this)

    member internal this.RecoveryOptions   
        with get() = RecoverFromFailuresAttribute.GetRecoveryOptions(this)
            
    static member Run(serviceFailureNotification:Action<string,int>) =
        let specs =
            let listServices () = 
                printfn "Services in %s:" (Process.Current.MainModule.FileName |> System.IO.Path.GetFileName)
                Base.Services |> Array.map(fun s -> s.ServiceName) |> Array.iteri(fun i n -> printfn "%i)\t%s" i n)

            let installHelper arg =
                trace "Install service" { ManagedInstallerClass.InstallHelper([| Process.Current.MainModule.FileName;arg |]) }

            let stopAllServices () =
                Base.AsyncStopAllServices () 
                |> Async.RunSynchronously
                |> List.iter(fun service -> printfn "Stopped service '%s'." service)

            let startAllServices () =
                Base.AsyncStartAllServices ()
                |> Async.RunSynchronously
                |> List.iter(fun service -> printfn "Started service '%s'." service)

            let recover (m:Map<string,string>) =
                trace "Recover service" {
                    let serviceName = m.["serviceName"]
                    let failCount = int(m.["failCount"])
                    serviceFailureNotification.Invoke(serviceName, failCount) }

            ["/l", ArgType.Unit (fun _ -> listServices ()), "Lists the services within the service executable"
             "/list", ArgType.Unit (fun _ -> listServices ()), "Lists the services within the service executable"
             "/i", ArgType.Unit (fun _ -> installHelper "-i"), "Installs the services within the service executable"
             "/install", ArgType.Unit (fun _ -> installHelper "-i"), "Installs the services within the service executable"
             "/u", ArgType.Unit (fun _ -> installHelper "-u"), "Uninstalls the services within the service executable"
             "/uninstall", ArgType.Unit (fun _ -> installHelper "-u"), "Uninstalls the services within the service executable"
             "/stopall", ArgType.Unit (fun _ -> stopAllServices ()), "Stops all running services within the service executable"
             "/startall", ArgType.Unit (fun _ -> startAllServices ()), "Stops all running services within the service executable"
             "recover:", ArgType.Map (fun m -> recover m), "Specifies the service failed"
            ] |> List.map (fun (sh, ty, desc) -> ArgInfo(sh, ty, desc))

        trace "WindowsServiceBase.Run" { 
            //If no arguments were matched fall back to running the service
            if  ArgParser.Parse(specs) |> Seq.length = 0 then Base.Run() }

    static member Run(serviceFailureNotification) = 
        WindowsServiceBase.Run(new Action<_,_>(fun a b -> serviceFailureNotification(a, b)))

    static member Run() = 
        let recover service failCount = 
            let log () = Logger.Default.Fatal.Write (sprintf "Service '%s' has failed %i time(s), manual intervention is required!" service failCount)
            log |> Fun.Try (fun _ -> ()) |> ignore        

        WindowsServiceBase.Run(new Action<_,_>(recover))